// SPDX-FileCopyrightText: Adam Evyčędo
//
// SPDX-License-Identifier: AGPL-3.0-or-later

package api

// TODO(BAF10) direction (0|1) to const (TO|BACK)

import (
	"apiote.xyz/p/szczanieckiej/traffic"
	"golang.org/x/text/language"

	"encoding/hex"
	"errors"
	"fmt"
	"log"
	"sort"
	"time"

	"git.sr.ht/~sircmpwn/go-bare"
	md "github.com/JohannesKaufmann/html-to-markdown"
	"github.com/adrg/strutil"
	"github.com/adrg/strutil/metrics"
	"github.com/dhconnelly/rtreego"
)

func convertTrafficAlerts(trafficAlerts []traffic.SpecificAlert) []AlertV1 {
	alerts := []AlertV1{}
	converter := md.NewConverter("", true, nil)
	for _, alert := range trafficAlerts {
		description, err := converter.ConvertString(alert.Description)
		if err != nil {
			log.Println("cannot convert html to markdown")
			description = alert.Description
		}
		a := AlertV1{
			Header:      alert.Header,
			Description: description,
			Url:         alert.URL,
			Cause:       makeAlertCauseV1(alert),
			Effect:      makeAlertEffectV1(alert),
		}
		alerts = append(alerts, a)
	}

	return alerts
}

func convertTrafficStop(stop traffic.Stop) StopV1 {
	s := StopV1{
		Code:          stop.Code,
		Name:          stop.NodeName,
		Zone:          stop.Zone,
		Position:      PositionV1{Lat: stop.Position.Lat, Lon: stop.Position.Lon},
		ChangeOptions: []ChangeOptionV1{},
	}
	for _, option := range stop.ChangeOptions {
		s.ChangeOptions = append(s.ChangeOptions, makeChangeOptionV1(option))
	}
	return s
}

func convertTrafficStopV2(stop traffic.Stop, feedID string) StopV2 {
	s := StopV2{
		Code:          stop.Code,
		Name:          stop.Name,
		NodeName:      stop.NodeName,
		Zone:          stop.Zone,
		FeedID:        feedID,
		Position:      PositionV1{Lat: stop.Position.Lat, Lon: stop.Position.Lon},
		ChangeOptions: []ChangeOptionV1{},
	}
	for _, option := range stop.ChangeOptions {
		s.ChangeOptions = append(s.ChangeOptions, makeChangeOptionV1(option))
	}
	return s
}

func convertTrafficStopStub(stopStub traffic.StopStub) StopStubV1 {
	s := StopStubV1{
		Code:     stopStub.Code,
		Name:     stopStub.Name,
		NodeName: stopStub.NodeName,
		Zone:     stopStub.Zone,
		OnDemand: stopStub.OnDemand,
	}
	return s
}

func convertTrafficVehicle(vehicle traffic.VehicleStatus, context traffic.Context, t *traffic.Traffic) (VehicleV1, error) {
	var (
		line traffic.Line
		err  error
	)
	if vehicle.LineID == "" {
		line, err = traffic.GetLineOld(vehicle.LineName, context, t)
		if err != nil {
			return VehicleV1{}, fmt.Errorf("while getting line %s: %w", vehicle.LineName, err)
		}
	} else {
		line, err = traffic.GetLine(vehicle.LineID, context, t)
		if err != nil {
			return VehicleV1{}, fmt.Errorf("while getting line %s: %w", vehicle.LineID, err)
		}
	}
	return VehicleV1{
		Id:              string(vehicle.VehicleID),
		Position:        PositionV1{vehicle.Latitude, vehicle.Longitude},
		Capabilities:    t.Vehicles[context.FeedID][context.Version][vehicle.VehicleID].Capabilities,
		Speed:           vehicle.Speed,
		Line:            LineStubV1{Name: line.Name, Kind: makeLineTypeV1(line), Colour: fromColor(line.Colour)},
		Headsign:        vehicle.Headsign,
		CongestionLevel: convertCongestionLevelV1(vehicle.CongestionLevel),
		OccupancyStatus: convertOccupancyStatusV1(vehicle.OccupancyStatus),
	}, nil
}

func convertTrafficVehicleV2(vehicle traffic.VehicleStatus, context traffic.Context, t *traffic.Traffic) (VehicleV2, error) {
	var (
		line traffic.Line
		err  error
	)
	if vehicle.LineID == "" {
		line, err = traffic.GetLineOld(vehicle.LineName, context, t)
		if err != nil {
			return VehicleV2{}, fmt.Errorf("while getting line %s: %w", vehicle.LineName, err)
		}
	} else {
		line, err = traffic.GetLine(vehicle.LineID, context, t)
		if err != nil {
			return VehicleV2{}, fmt.Errorf("while getting line %s: %w", vehicle.LineID, err)
		}
	}
	return VehicleV2{
		Id:              string(vehicle.VehicleID),
		Position:        PositionV1{vehicle.Latitude, vehicle.Longitude},
		Capabilities:    t.Vehicles[context.FeedID][context.Version][vehicle.VehicleID].Capabilities,
		Speed:           vehicle.Speed,
		Line:            LineStubV2{Name: line.Name, Kind: makeLineTypeV2(line), Colour: fromColor(line.Colour)},
		Headsign:        vehicle.Headsign,
		CongestionLevel: convertCongestionLevelV1(vehicle.CongestionLevel),
		OccupancyStatus: convertOccupancyStatusV1(vehicle.OccupancyStatus),
	}, nil
}

func convertTrafficVehicleV3(vehicle traffic.VehicleStatus, context traffic.Context, t *traffic.Traffic) (VehicleV3, error) {
	var (
		line traffic.Line
		err  error
	)
	if vehicle.LineID == "" {
		line, err = traffic.GetLineOld(vehicle.LineName, context, t)
		if err != nil {
			return VehicleV3{}, fmt.Errorf("while getting line %s: %w", vehicle.LineName, err)
		}
	} else {
		line, err = traffic.GetLine(vehicle.LineID, context, t)
		if err != nil {
			return VehicleV3{}, fmt.Errorf("while getting line %s: %w", vehicle.LineID, err)
		}
	}
	return VehicleV3{
		Id:              string(vehicle.VehicleID),
		Position:        PositionV1{vehicle.Latitude, vehicle.Longitude},
		Capabilities:    t.Vehicles[context.FeedID][context.Version][vehicle.VehicleID].Capabilities,
		Speed:           vehicle.Speed,
		Line:            LineStubV3{Name: line.Name, Kind: makeLineTypeV3(line), Colour: fromColor(line.Colour)},
		Headsign:        vehicle.Headsign,
		CongestionLevel: convertCongestionLevelV1(vehicle.CongestionLevel),
		OccupancyStatus: convertOccupancyStatusV1(vehicle.OccupancyStatus),
	}, nil
}

func getDayOffset(t1, t2 time.Time) int8 {
	midnightBefore := time.Date(t1.Year(), t1.Month(), t1.Day(), 0, 0, 0, 0, t1.Location())
	tomorrow := t1.AddDate(0, 0, 1)
	midnightAfter := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 0, 0, 0, 0, tomorrow.Location())
	if t2.Before(midnightBefore) {
		return -1
	} else if t2.Before(midnightAfter) {
		return 0
	} else {
		return 1
	}
}

func marshalStopOrder(tripOffset uint, stopOrder int) (string, error) {
	order := traffic.StopOrder{
		TripOffset: tripOffset,
		Sequence:   stopOrder,
	}
	bytes, err := bare.Marshal(&order)
	return hex.EncodeToString(bytes), err
}

func CreateSuccessTrip(trafficTrip []traffic.TimedStopStub) /*TripResponse*/ {
	/*trip := []TimedStopStub{}
	for _, s := range trafficTrip {
		trip = append(trip, TimedStopStub{
			StopStub: StopStub{
				Code:     s.Code,
				Name:     s.Name,
				Zone:     s.Zone,
				OnDemand: s.OnDemand,
			},
			Time: s.Time,
		})
	}
	return SuccessTrip{
		Trip: trip,
	}*/
}

func CreateSuccessVehicle(vehicles []traffic.Vehicle, context traffic.Context, t *traffic.Traffic) /*(VehicleResponse, error)*/ {
	/*v := make([]Vehicle, len(vehicles))
	var err error = nil
	for i, vehicle := range vehicles {
		v[i], err = convertTrafficVehicle(vehicle, context, t)
		if err != nil {
			return SuccessVehicle{}, fmt.Errorf("while converting Traffic Vehicle %s: %w", vehicle.Id, err)
		}
	}
	return SuccessVehicle{
		Vehicles: v,
	}, nil*/
}

func CreateSuccessQueryables(items []traffic.Queryable, context traffic.Context, t *traffic.Traffic, other QueryablesResponse, sortByDistance bool) (QueryablesResponse, error) {
	success := QueryablesResponseV1{
		Queryables: []QueryableV1{},
	}
	for _, item := range items {
		if stop, ok := item.(traffic.Stop); ok {
			s := convertTrafficStop(stop)
			success.Queryables = append(success.Queryables, QueryableV1(s))
		} else {
			continue
		}
	}
	if otherV1, ok := other.(QueryablesResponseV1); ok {
		success.Queryables = append(success.Queryables, otherV1.Queryables...)
	} else {
		return success, errors.New("wrong version of other")
	}
	return success, nil
}

func CreateSuccessQueryablesV2(query string, items []traffic.Queryable, context traffic.Context, t *traffic.Traffic, other QueryablesResponse, sortByDistance bool) (QueryablesResponse, error) {
	success := QueryablesResponseV2{
		Queryables: []QueryableV2{},
	}
	for _, item := range items {
		if stop, ok := item.(traffic.Stop); ok {
			s := convertTrafficStopV2(stop, context.FeedID)
			success.Queryables = append(success.Queryables, QueryableV2(s))
		} else if line, ok := item.(traffic.Line); ok {
			l := convertTrafficLine(line, context.FeedID)
			success.Queryables = append(success.Queryables, QueryableV2(l))
		} else {
			continue
		}
	}
	if otherV2, ok := other.(QueryablesResponseV2); ok {
		success.Queryables = append(success.Queryables, otherV2.Queryables...)
	} else {
		return success, errors.New("wrong version of other")
	}
	if sortByDistance {
		success.Queryables = sortQueryablesByDistanceV2(query, success.Queryables)
	} else {
		success.Queryables = sortQueryables(query, success.Queryables)
	}
	return success, nil
}

func CreateSuccessQueryablesV3(query string, items []traffic.Queryable, context traffic.Context, t *traffic.Traffic, other QueryablesResponse, sortByDistance bool) (QueryablesResponse, error) {
	success := QueryablesResponseV3{
		Queryables: []QueryableV3{},
	}
	for _, item := range items {
		if stop, ok := item.(traffic.Stop); ok {
			s := convertTrafficStopV2(stop, context.FeedID)
			success.Queryables = append(success.Queryables, QueryableV3(s))
		} else if line, ok := item.(traffic.Line); ok {
			l := convertTrafficLineV2(line, context.FeedID)
			success.Queryables = append(success.Queryables, QueryableV3(l))
		} else {
			continue
		}
	}
	if otherV3, ok := other.(QueryablesResponseV3); ok {
		success.Queryables = append(success.Queryables, otherV3.Queryables...)
	} else {
		return success, errors.New("wrong version of other")
	}
	if sortByDistance {
		success.Queryables = sortQueryablesByDistanceV3(query, success.Queryables)
	} else {
		success.Queryables = sortQueryablesV3(query, success.Queryables)
	}
	return success, nil
}

func CreateSuccessQueryablesV4(query string, items []traffic.Queryable, context traffic.Context, t *traffic.Traffic, other QueryablesResponse, sortByDistance bool) (QueryablesResponse, error) {
	success := QueryablesResponseV4{
		Queryables: []QueryableV4{},
	}
	for _, item := range items {
		if stop, ok := item.(traffic.Stop); ok {
			s := convertTrafficStopV2(stop, context.FeedID)
			success.Queryables = append(success.Queryables, QueryableV4(s))
		} else if line, ok := item.(traffic.Line); ok {
			if len(line.Headsigns) > 0 {
				l := convertTrafficLineV3(line, context.FeedID)
				success.Queryables = append(success.Queryables, QueryableV4(l))
			}
		} else {
			continue
		}
	}
	if otherV4, ok := other.(QueryablesResponseV4); ok {
		success.Queryables = append(success.Queryables, otherV4.Queryables...)
	} else {
		return success, errors.New("wrong version of other")
	}
	if sortByDistance {
		success.Queryables = sortQueryablesByDistanceV4(query, success.Queryables)
	} else {
		success.Queryables = sortQueryablesV4(query, success.Queryables)
	}
	return success, nil
}

func LimitQueryables(r QueryablesResponse, offset, limit uint64) QueryablesResponse {
	var result QueryablesResponse

	switch r.(type) {
	case QueryablesResponseV1:
		var queryables []QueryableV1
		if int(offset) > len(r.(QueryablesResponseV1).Queryables) {
			queryables = []QueryableV1{}
		} else if len(r.(QueryablesResponseV1).Queryables) < int(offset+limit) {
			queryables = r.(QueryablesResponseV1).Queryables[offset:]
		} else {
			queryables = r.(QueryablesResponseV1).Queryables[offset : offset+limit]
		}
		result = QueryablesResponseV1{queryables}
	case QueryablesResponseV2:
		var queryables []QueryableV2
		if int(offset) > len(r.(QueryablesResponseV2).Queryables) {
			queryables = []QueryableV2{}
		} else if len(r.(QueryablesResponseV2).Queryables) < int(offset+limit) {
			queryables = r.(QueryablesResponseV2).Queryables[offset:]
		} else {
			queryables = r.(QueryablesResponseV2).Queryables[offset : offset+limit]
		}
		result = QueryablesResponseV2{queryables}
	case QueryablesResponseV3:
		var queryables []QueryableV3
		if int(offset) > len(r.(QueryablesResponseV3).Queryables) {
			queryables = []QueryableV3{}
		} else if len(r.(QueryablesResponseV3).Queryables) < int(offset+limit) {
			queryables = r.(QueryablesResponseV3).Queryables[offset:]
		} else {
			queryables = r.(QueryablesResponseV3).Queryables[offset : offset+limit]
		}
		result = QueryablesResponseV3{queryables}
	case QueryablesResponseV4:
		var queryables []QueryableV4
		if int(offset) > len(r.(QueryablesResponseV4).Queryables) {
			queryables = []QueryableV4{}
		} else if len(r.(QueryablesResponseV4).Queryables) < int(offset+limit) {
			queryables = r.(QueryablesResponseV4).Queryables[offset:]
		} else {
			queryables = r.(QueryablesResponseV4).Queryables[offset : offset+limit]
		}
		result = QueryablesResponseV4{queryables}
	}

	return result
}

func sortQueryables(query string, queryables []QueryableV2) []QueryableV2 {
	// fixme query and names should be cleaned
	sort.Slice(queryables, func(i, j int) bool {
		var nameI, nameJ string

		switch queryables[i].(type) {
		case StopV2:
			nameI = queryables[i].(StopV2).Name
		case LineV1:
			nameI = queryables[i].(LineV1).Name
		}
		switch queryables[j].(type) {
		case StopV2:
			nameJ = queryables[j].(StopV2).Name
		case LineV1:
			nameJ = queryables[j].(LineV1).Name
		}
		levenshtein := &metrics.Levenshtein{
			CaseSensitive: false,
			InsertCost:    1,
			DeleteCost:    1,
			ReplaceCost:   1,
		}
		distance1 := strutil.Similarity(query, nameI, levenshtein)
		distance2 := strutil.Similarity(query, nameJ, levenshtein)
		return distance1 > distance2
	})
	return queryables
}

func sortQueryablesByDistanceV2(query string, queryables []QueryableV2) []QueryableV2 {
	queryPosition, err := traffic.ParsePosition(query)
	if err != nil {
		log.Printf("while parsing position %s: %v\n", query, err)
		return queryables
	}

	stops := []StopV2{}
	for _, q := range queryables {
		if s, ok := q.(StopV2); ok {
			stops = append(stops, s)
		}
	}
	tree := rtreego.NewTree(2, 1, 50)
	for _, stop := range stops {
		tree.Insert(stop)
	}
	spatials := tree.NearestNeighbors(12, rtreego.Point{queryPosition.Lat, queryPosition.Lon})
	queryables = make([]QueryableV2, len(spatials))
	for i, spatial := range spatials {
		queryables[i] = spatial.(StopV2)
	}

	return queryables
}

func sortQueryablesV3(query string, queryables []QueryableV3) []QueryableV3 {
	// fixme query and names should be cleaned
	sort.Slice(queryables, func(i, j int) bool {
		var nameI, nameJ string

		switch queryables[i].(type) {
		case StopV2:
			nameI = queryables[i].(StopV2).Name
		case LineV2:
			nameI = queryables[i].(LineV2).Name
		}
		switch queryables[j].(type) {
		case StopV2:
			nameJ = queryables[j].(StopV2).Name
		case LineV2:
			nameJ = queryables[j].(LineV2).Name
		}
		levenshtein := &metrics.Levenshtein{
			CaseSensitive: false,
			InsertCost:    1,
			DeleteCost:    1,
			ReplaceCost:   1,
		}
		distance1 := strutil.Similarity(query, nameI, levenshtein)
		distance2 := strutil.Similarity(query, nameJ, levenshtein)
		return distance1 > distance2
	})
	return queryables
}

func sortQueryablesByDistanceV3(query string, queryables []QueryableV3) []QueryableV3 {
	queryPosition, err := traffic.ParsePosition(query)
	if err != nil {
		log.Printf("while parsing position %s: %v\n", query, err)
		return queryables
	}

	stops := []StopV2{}
	for _, q := range queryables {
		if s, ok := q.(StopV2); ok {
			stops = append(stops, s)
		}
	}
	tree := rtreego.NewTree(2, 1, 50)
	for _, stop := range stops {
		tree.Insert(stop)
	}
	spatials := tree.NearestNeighbors(12, rtreego.Point{queryPosition.Lat, queryPosition.Lon})
	queryables = make([]QueryableV3, len(spatials))
	for i, spatial := range spatials {
		queryables[i] = spatial.(StopV2)
	}

	return queryables
}

func sortQueryablesV4(query string, queryables []QueryableV4) []QueryableV4 {
	// fixme query and names should be cleaned
	sort.Slice(queryables, func(i, j int) bool {
		var nameI, nameJ string

		switch queryables[i].(type) {
		case StopV2:
			nameI = queryables[i].(StopV2).Name
		case LineV3:
			nameI = queryables[i].(LineV3).Name
		}
		switch queryables[j].(type) {
		case StopV2:
			nameJ = queryables[j].(StopV2).Name
		case LineV3:
			nameJ = queryables[j].(LineV3).Name
		}
		levenshtein := &metrics.Levenshtein{
			CaseSensitive: false,
			InsertCost:    1,
			DeleteCost:    1,
			ReplaceCost:   1,
		}
		distance1 := strutil.Similarity(query, nameI, levenshtein)
		distance2 := strutil.Similarity(query, nameJ, levenshtein)
		return distance1 > distance2
	})
	return queryables
}

func sortQueryablesByDistanceV4(query string, queryables []QueryableV4) []QueryableV4 {
	queryPosition, err := traffic.ParsePosition(query)
	if err != nil {
		log.Printf("while parsing position %s: %v\n", query, err)
		return queryables
	}

	stops := []StopV2{}
	for _, q := range queryables {
		if s, ok := q.(StopV2); ok {
			stops = append(stops, s)
		}
	}
	tree := rtreego.NewTree(2, 1, 50)
	for _, stop := range stops {
		tree.Insert(stop)
	}
	spatials := tree.NearestNeighbors(12, rtreego.Point{queryPosition.Lat, queryPosition.Lon})
	queryables = make([]QueryableV4, len(spatials))
	for i, spatial := range spatials {
		queryables[i] = spatial.(StopV2)
	}

	return queryables
}

func CreateSuccessLocatables(locatables []traffic.Locatable, context traffic.Context, t *traffic.Traffic, other LocatablesResponse) (LocatablesResponse, error) {
	success := LocatablesResponseV1{
		Locatables: []LocatableV1{},
	}
	for _, locatable := range locatables {
		if stop, ok := locatable.(traffic.Stop); ok {
			s := convertTrafficStop(stop)
			success.Locatables = append(success.Locatables, LocatableV1(s))
		} else if vehicle, ok := locatable.(traffic.VehicleStatus); ok {
			v, err := convertTrafficVehicle(vehicle, context, t)
			if err != nil {
				return success, fmt.Errorf("while converting Traffic Vehicle %s: %w", vehicle.VehicleID, err)
			}
			success.Locatables = append(success.Locatables, LocatableV1(v))
		}
	}
	if otherV1, ok := other.(LocatablesResponseV1); ok {
		success.Locatables = append(success.Locatables, otherV1.Locatables...)
	} else {
		return success, errors.New("wrong version of other")
	}
	return success, nil
}

func CreateSuccessLocatablesV2(locatables []traffic.Locatable, context traffic.Context, t *traffic.Traffic, other LocatablesResponse) (LocatablesResponse, error) {
	success := LocatablesResponseV2{
		Locatables: []LocatableV2{},
	}
	for _, locatable := range locatables {
		if stop, ok := locatable.(traffic.Stop); ok {
			s := convertTrafficStopV2(stop, context.FeedID)
			success.Locatables = append(success.Locatables, LocatableV2(s))
		} else if vehicle, ok := locatable.(traffic.VehicleStatus); ok {
			v, err := convertTrafficVehicleV2(vehicle, context, t)
			if err != nil {
				return success, fmt.Errorf("while converting Traffic Vehicle %s: %w", vehicle.VehicleID, err)
			}
			success.Locatables = append(success.Locatables, LocatableV2(v))
		}
	}
	if otherV2, ok := other.(LocatablesResponseV2); ok {
		success.Locatables = append(success.Locatables, otherV2.Locatables...)
	} else {
		return success, errors.New("wrong version of other")
	}
	return success, nil
}

func CreateSuccessLocatablesV3(locatables []traffic.Locatable, context traffic.Context, t *traffic.Traffic, other LocatablesResponse) (LocatablesResponse, error) {
	success := LocatablesResponseV3{
		Locatables: []LocatableV3{},
	}
	for _, locatable := range locatables {
		if stop, ok := locatable.(traffic.Stop); ok {
			s := convertTrafficStopV2(stop, context.FeedID)
			success.Locatables = append(success.Locatables, LocatableV3(s))
		} else if vehicle, ok := locatable.(traffic.VehicleStatus); ok {
			v, err := convertTrafficVehicleV3(vehicle, context, t)
			if err != nil {
				return success, fmt.Errorf("while converting Traffic Vehicle %s: %w", vehicle.VehicleID, err)
			}
			success.Locatables = append(success.Locatables, LocatableV3(v))
		}
	}
	if otherV3, ok := other.(LocatablesResponseV3); ok {
		success.Locatables = append(success.Locatables, otherV3.Locatables...)
	} else {
		return success, errors.New("wrong version of other")
	}
	return success, nil
}

func convertVehicleStatusV1(status traffic.DepartureStatus, timeToArrival float64) VehicleStatusV1 {
	if status == traffic.AT_STOP {
		return STATUS_AT_STOP
	} else if timeToArrival < 0 {
		return STATUS_DEPARTED
	} else if timeToArrival < 1 {
		return STATUS_INCOMING
	}
	return STATUS_IN_TRANSIT
}

func convertCongestionLevelV1(level traffic.CongestionLevel) CongestionLevelV1 {
	switch level {
	case traffic.CONGESTION_UNKNOWN:
		return CONGESTION_UNKNOWN
	case traffic.CONGESTION_SMOOTH:
		return CONGESTION_SMOOTH
	case traffic.CONGESTION_STOP_AND_GO:
		return CONGESTION_STOP_AND_GO
	case traffic.CONGESTION_SIGNIFICANT:
		return CONGESTION_SIGNIFICANT
	case traffic.CONGESTION_SEVERE:
		return CONGESTION_SEVERE
	default:
		return CONGESTION_UNKNOWN
	}
}

func convertOccupancyStatusV1(status traffic.OccupancyStatus) OccupancyStatusV1 {
	switch status {
	case traffic.OCCUPANCY_UNKNOWN:
		return OCCUPANCY_UNKNOWN
	case traffic.OCCUPANCY_EMPTY:
		return OCCUPANCY_EMPTY
	case traffic.OCCUPANCY_MANY_AVAILABLE:
		return OCCUPANCY_MANY_AVAILABLE
	case traffic.OCCUPANCY_FEW_AVAILABLE:
		return OCCUPANCY_FEW_AVAILABLE
	case traffic.OCCUPANCY_STANDING_ONLY:
		return OCCUPANCY_STANDING_ONLY
	case traffic.OCCUPANCY_CRUSHED:
		return OCCUPANCY_CRUSHED
	case traffic.OCCUPANCY_FULL:
		return OCCUPANCY_FULL
	case traffic.OCCUPANCY_NOT_ACCEPTING:
		return OCCUPANCY_NOT_ACCEPTING
	default:
		return OCCUPANCY_UNKNOWN
	}
}

func CreateSuccessDeparturesV1(stop traffic.Stop, departures []traffic.DepartureRealtime, date time.Time, vehicles map[string]traffic.Vehicle, alerts []traffic.SpecificAlert, ctx traffic.Context, t *traffic.Traffic, accept map[uint]struct{}, preferredLanguages []language.Tag) (DeparturesResponse, error) {
	d := []DepartureV1{}
	var success DeparturesResponse
	now := time.Now()
	timezone, err := traffic.GetTimezone(stop, t, ctx.FeedID)
	if err != nil {
		return success, err
	}
	datetime := time.Date(date.Year(), date.Month(),
		date.Day(), now.Hour(), now.Minute(), now.Second(), 0, now.Location()).In(timezone)
	for _, trafficDeparture := range departures {
		zoneAbbr := trafficDeparture.Time.Location().String()
		stopOrder, err := marshalStopOrder(trafficDeparture.Order.TripOffset, trafficDeparture.Order.Sequence)
		if err != nil {
			return success, err
		}
		vehicle, err := convertTrafficVehicle(trafficDeparture.Update.VehicleStatus, ctx, t)
		if err != nil {
			return success, fmt.Errorf("while converting vehicle status: %w", err)
		}
		departureTime := traffic.GetTimeWithDelay(trafficDeparture)
		departure := DepartureV1{
			Id: stopOrder,
			Time: TimeV1{
				DayOffset: getDayOffset(datetime, departureTime),
				Hour:      uint8(departureTime.Hour()),
				Minute:    uint8(departureTime.Minute()),
				Second:    uint8(departureTime.Second()),
				Zone:      zoneAbbr,
			},
			Status:     STATUS_IN_TRANSIT,
			IsRealtime: trafficDeparture.Update.TimetableRelationship != traffic.NO_TRIP_DATA && trafficDeparture.Update.TimetableRelationship != traffic.NOT_REALTIME,
			Vehicle:    vehicle,
			Boarding:   makeBoardingV1(trafficDeparture.Departure.Pickup, trafficDeparture.Departure.Dropoff),
		}
		timeToArrival := departureTime.Sub(datetime).Minutes()
		if departure.IsRealtime {
			departure.Status = convertVehicleStatusV1(trafficDeparture.Update.VehicleStatus.Status, timeToArrival)
		}
		d = append(d, departure)
	}
	success = DeparturesResponseV1{
		Stop:       convertTrafficStop(stop),
		Departures: d,
		Alerts:     convertTrafficAlerts(alerts),
	}
	return success, nil
}

func CreateSuccessDeparturesV2(stop traffic.Stop, departures []traffic.DepartureRealtime, date time.Time, vehicles map[string]traffic.Vehicle, alerts []traffic.SpecificAlert, ctx traffic.Context, t *traffic.Traffic, accept map[uint]struct{}, preferredLanguages []language.Tag) (DeparturesResponse, error) {
	d := []DepartureV2{}
	var success DeparturesResponse
	now := time.Now()
	timezone, err := traffic.GetTimezone(stop, t, ctx.FeedID)
	if err != nil {
		return success, err
	}
	datetime := time.Date(date.Year(), date.Month(),
		date.Day(), now.Hour(), now.Minute(), now.Second(), 0, now.Location()).In(timezone)
	for _, trafficDeparture := range departures {
		zoneAbbr := trafficDeparture.Time.Location().String()
		stopOrder, err := marshalStopOrder(trafficDeparture.Order.TripOffset, trafficDeparture.Order.Sequence)
		if err != nil {
			return success, err
		}
		vehicle, err := convertTrafficVehicleV2(trafficDeparture.Update.VehicleStatus, ctx, t)
		if err != nil {
			return success, fmt.Errorf("while converting vehicle status: %w", err)
		}
		departureTime := traffic.GetTimeWithDelay(trafficDeparture)
		departure := DepartureV2{
			Id: stopOrder,
			Time: TimeV1{
				DayOffset: getDayOffset(datetime, departureTime),
				Hour:      uint8(departureTime.Hour()),
				Minute:    uint8(departureTime.Minute()),
				Second:    uint8(departureTime.Second()),
				Zone:      zoneAbbr,
			},
			Status:     STATUS_IN_TRANSIT,
			IsRealtime: trafficDeparture.Update.TimetableRelationship != traffic.NO_TRIP_DATA && trafficDeparture.Update.TimetableRelationship != traffic.NOT_REALTIME,
			Vehicle:    vehicle,
			Boarding:   makeBoardingV1(trafficDeparture.Departure.Pickup, trafficDeparture.Departure.Dropoff),
		}
		timeToArrival := departureTime.Sub(datetime).Minutes()
		if departure.IsRealtime {
			departure.Status = convertVehicleStatusV1(trafficDeparture.Update.VehicleStatus.Status, timeToArrival)
		}
		d = append(d, departure)
	}
	success = DeparturesResponseV2{
		Stop:       convertTrafficStopV2(stop, ctx.FeedID),
		Departures: d,
		Alerts:     convertTrafficAlerts(alerts),
	}
	return success, nil
}

func CreateSuccessDeparturesV3(stop traffic.Stop, departures []traffic.DepartureRealtime, date time.Time, vehicles map[string]traffic.Vehicle, alerts []traffic.SpecificAlert, ctx traffic.Context, t *traffic.Traffic, accept map[uint]struct{}, preferredLanguages []language.Tag) (DeparturesResponse, error) {
	d := []DepartureV3{}
	var success DeparturesResponse
	now := time.Now()
	timezone, err := traffic.GetTimezone(stop, t, ctx.FeedID)
	if err != nil {
		return success, err
	}
	datetime := time.Date(date.Year(), date.Month(),
		date.Day(), now.Hour(), now.Minute(), now.Second(), 0, now.Location()).In(timezone)
	for _, trafficDeparture := range departures {
		zoneAbbr := trafficDeparture.Time.Location().String()
		stopOrder, err := marshalStopOrder(trafficDeparture.Order.TripOffset, trafficDeparture.Order.Sequence)
		if err != nil {
			return success, err
		}
		vehicle, err := convertTrafficVehicleV3(trafficDeparture.Update.VehicleStatus, ctx, t)
		if err != nil {
			return success, fmt.Errorf("while converting vehicle status: %w", err)
		}
		departureTime := traffic.GetTimeWithDelay(trafficDeparture)
		departure := DepartureV3{
			Id: stopOrder,
			Time: TimeV1{
				DayOffset: getDayOffset(datetime, departureTime),
				Hour:      uint8(departureTime.Hour()),
				Minute:    uint8(departureTime.Minute()),
				Second:    uint8(departureTime.Second()),
				Zone:      zoneAbbr,
			},
			Status:     STATUS_IN_TRANSIT,
			IsRealtime: trafficDeparture.Update.TimetableRelationship != traffic.NO_TRIP_DATA && trafficDeparture.Update.TimetableRelationship != traffic.NOT_REALTIME,
			Vehicle:    vehicle,
			Boarding:   makeBoardingV1(trafficDeparture.Departure.Pickup, trafficDeparture.Departure.Dropoff),
		}
		timeToArrival := departureTime.Sub(datetime).Minutes()
		if departure.IsRealtime {
			departure.Status = convertVehicleStatusV1(trafficDeparture.Update.VehicleStatus.Status, timeToArrival)
		}
		d = append(d, departure)
	}
	success = DeparturesResponseV3{
		Stop:       convertTrafficStopV2(stop, ctx.FeedID),
		Departures: d,
		Alerts:     convertTrafficAlerts(alerts),
	}
	return success, nil
}

func CreateSuccessDeparturesV4(stop traffic.Stop, departures []traffic.DepartureRealtime, date time.Time, vehicles map[string]traffic.Vehicle, alerts []traffic.SpecificAlert, ctx traffic.Context, t *traffic.Traffic, accept map[uint]struct{}, preferredLanguages []language.Tag) (DeparturesResponse, error) {
	d := []DepartureV4{}
	var success DeparturesResponse
	now := time.Now()
	timezone, err := traffic.GetTimezone(stop, t, ctx.FeedID)
	if err != nil {
		return success, err
	}
	datetime := time.Date(date.Year(), date.Month(),
		date.Day(), now.Hour(), now.Minute(), now.Second(), 0, now.Location()).In(timezone)
	for _, trafficDeparture := range departures {
		zoneAbbr := trafficDeparture.Time.Location().String()
		stopOrder, err := marshalStopOrder(trafficDeparture.Order.TripOffset, trafficDeparture.Order.Sequence)
		if err != nil {
			return success, err
		}
		vehicle, err := convertTrafficVehicleV3(trafficDeparture.Update.VehicleStatus, ctx, t)
		if err != nil {
			return success, fmt.Errorf("while converting vehicle status: %w", err)
		}
		departureTime := traffic.GetTimeWithDelay(trafficDeparture)
		departure := DepartureV4{
			Id: stopOrder,
			Time: TimeV1{
				DayOffset: getDayOffset(datetime, departureTime),
				Hour:      uint8(departureTime.Hour()),
				Minute:    uint8(departureTime.Minute()),
				Second:    uint8(departureTime.Second()),
				Zone:      zoneAbbr,
			},
			Status:     STATUS_IN_TRANSIT,
			IsRealtime: trafficDeparture.Update.TimetableRelationship != traffic.NO_TRIP_DATA && trafficDeparture.Update.TimetableRelationship != traffic.NOT_REALTIME,
			Vehicle:    vehicle,
			Boarding:   makeBoardingV1(trafficDeparture.Departure.Pickup, trafficDeparture.Departure.Dropoff),
			Alerts:     convertTrafficAlerts(trafficDeparture.Alerts),
		}
		timeToArrival := departureTime.Sub(datetime).Minutes()
		if departure.IsRealtime {
			departure.Status = convertVehicleStatusV1(trafficDeparture.Update.VehicleStatus.Status, timeToArrival)
		}
		d = append(d, departure)
	}
	success = DeparturesResponseV4{
		Stop:       convertTrafficStopV2(stop, ctx.FeedID),
		Departures: d,
		Alerts:     convertTrafficAlerts(alerts),
	}
	return success, nil
}

func CreateSuccessDeparturesDev(stop traffic.Stop, departures []traffic.DepartureRealtime, date time.Time, vehicles map[string]traffic.Vehicle, alerts []traffic.SpecificAlert, ctx traffic.Context, t *traffic.Traffic, accept map[uint]struct{}, preferredLanguages []language.Tag) (DeparturesResponse, error) {
	d := []DepartureV4{}
	var success DeparturesResponse
	now := time.Now()
	timezone, err := traffic.GetTimezone(stop, t, ctx.FeedID)
	if err != nil {
		return success, err
	}
	datetime := time.Date(date.Year(), date.Month(),
		date.Day(), now.Hour(), now.Minute(), now.Second(), 0, now.Location()).In(timezone)
	for _, trafficDeparture := range departures {
		zoneAbbr := trafficDeparture.Time.Location().String()
		stopOrder, err := marshalStopOrder(trafficDeparture.Order.TripOffset, trafficDeparture.Order.Sequence)
		if err != nil {
			return success, err
		}
		vehicle, err := convertTrafficVehicleV3(trafficDeparture.Update.VehicleStatus, ctx, t)
		if err != nil {
			return success, fmt.Errorf("while converting vehicle status: %w", err)
		}
		departureTime := traffic.GetTimeWithDelay(trafficDeparture)
		departure := DepartureV4{
			Id: stopOrder,
			Time: TimeV1{
				DayOffset: getDayOffset(datetime, departureTime),
				Hour:      uint8(departureTime.Hour()),
				Minute:    uint8(departureTime.Minute()),
				Second:    uint8(departureTime.Second()),
				Zone:      zoneAbbr,
			},
			Status:     STATUS_IN_TRANSIT,
			IsRealtime: trafficDeparture.Update.TimetableRelationship != traffic.NO_TRIP_DATA && trafficDeparture.Update.TimetableRelationship != traffic.NOT_REALTIME,
			Vehicle:    vehicle,
			Boarding:   makeBoardingV1(trafficDeparture.Departure.Pickup, trafficDeparture.Departure.Dropoff),
			Alerts:     convertTrafficAlerts(trafficDeparture.Alerts),
		}
		timeToArrival := departureTime.Sub(datetime).Minutes()
		if departure.IsRealtime {
			departure.Status = convertVehicleStatusV1(trafficDeparture.Update.VehicleStatus.Status, timeToArrival)
		}
		d = append(d, departure)
	}
	success = DeparturesResponseDev{
		Stop:       convertTrafficStopV2(stop, ctx.FeedID),
		Departures: d,
		Alerts:     convertTrafficAlerts(alerts),
	}
	return success, nil
}

func fromColor(c traffic.Colour) ColourV1 {
	return ColourV1{
		R: c.R,
		G: c.G,
		B: c.B,
	}
}

func makeLineTypeV1(line traffic.Line) LineTypeV1 {
	if line.Kind == traffic.TRAM {
		return TRAM
	} else if line.Kind == traffic.BUS {
		return BUS
	} else {
		return LINE_UNKNOWN
	}
}

func makeLineTypeV2(line traffic.Line) LineTypeV2 {
	if line.Kind == traffic.TRAM {
		return LINE_V2_TRAM
	} else if line.Kind == traffic.BUS {
		return LINE_V2_BUS
	} else if line.Kind == traffic.TROLLEYBUS {
		return LINE_V2_TROLLEYBUS
	} else {
		return LINE_V2_UNKNOWN
	}
}

func makeLineTypeV3(line traffic.Line) LineTypeV3 {
	switch line.Kind {
	case traffic.TRAM:
		return LINE_V3_TRAM
	case traffic.BUS:
		return LINE_V3_BUS
	case traffic.TROLLEYBUS:
		return LINE_V3_TROLLEYBUS
	case traffic.METRO:
		return LINE_V3_METRO
	case traffic.RAIL:
		return LINE_V3_RAIL
	case traffic.FERRY:
		return LINE_V3_FERRY
	case traffic.CABLE_TRAM:
		return LINE_V3_CABLE_TRAM
	case traffic.CABLE_CAR:
		return LINE_V3_CABLE_CAR
	case traffic.FUNICULAR:
		return LINE_V3_FUNICULAR
	case traffic.MONORAIL:
		return LINE_V3_MONORAIL
	default:
		return LINE_V3_UNKNOWN
	}
}

func makeChangeOptionV1(option traffic.ChangeOption) ChangeOptionV1 {
	return ChangeOptionV1{
		LineName: option.LineName,
		Headsign: option.Headsign,
	}
}

func makeBoardingV1(pickup, dropoff traffic.Boarding) uint8 {
	b := BOARDING_NONE
	if pickup == traffic.REGULAR {
		b |= ONBOARDING_REGULAR
	} else if pickup == traffic.BY_PHONE {
		b |= ONBOARDING_PHONE
	} else if pickup == traffic.BY_DRIVER {
		b |= ONBOARDING_DRIVER
	}
	if dropoff == traffic.REGULAR {
		b |= OFFBOARDING_REGULAR
	} else if dropoff == traffic.BY_PHONE {
		b |= OFFBOARDING_PHONE
	} else if dropoff == traffic.BY_DRIVER {
		b |= OFFBOARDING_DRIVER
	}
	return uint8(b)
}

func makeAlertCauseV1(alert traffic.SpecificAlert) AlertCauseV1 {
	switch alert.Cause {
	case 0:
		return CAUSE_UNKNOWN
	case 1:
		return CAUSE_OTHER
	case 2:
		return CAUSE_TECHNICAL_PROBLEM
	case 3:
		return CAUSE_STRIKE
	case 4:
		return CAUSE_DEMONSTRATION
	case 5:
		return CAUSE_ACCIDENT
	case 6:
		return CAUSE_HOLIDAY
	case 7:
		return CAUSE_WEATHER
	case 8:
		return CAUSE_MAINTENANCE
	case 9:
		return CAUSE_CONSTRUCTION
	case 10:
		return CAUSE_POLICE_ACTIVITY
	case 11:
		return CAUSE_MEDICAL_EMERGENCY
	default:
		return CAUSE_UNKNOWN
	}
}
func makeAlertEffectV1(alert traffic.SpecificAlert) AlertEffectV1 {
	switch alert.Effect {
	case 0:
		return EFFECT_NO_SERVICE
	case 1:
		return EFFECT_REDUCED_SERVICE
	case 2:
		return EFFECT_SIGNIFICANT_DELAYS
	case 3:
		return EFFECT_DETOUR
	case 4:
		return EFFECT_ADDITIONAL_SERVICE
	case 5:
		return EFFECT_MODIFIED_SERVICE
	case 6:
		return EFFECT_OTHER
	case 7:
		return EFFECT_UNKNOWN
	case 8:
		return EFFECT_STOP_MOVED
	case 9:
		return EFFECT_NONE
	case 10:
		return EFFECT_ACCESSIBILITY_ISSUE
	default:
		return EFFECT_UNKNOWN
	}
}
